/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.silverpeas.core.calendar.icalendar;

import org.apache.commons.collections4.map.HashedMap;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.admin.user.service.UserProvider;
import org.silverpeas.core.calendar.Attendee;
import org.silverpeas.core.calendar.CalendarComponent;
import org.silverpeas.core.calendar.CalendarEvent;
import org.silverpeas.core.calendar.CalendarEventOccurrence;
import org.silverpeas.core.calendar.CalendarEventStubBuilder;
import org.silverpeas.core.calendar.DayOfWeekOccurrence;
import org.silverpeas.core.calendar.Priority;
import org.silverpeas.core.calendar.Recurrence;
import org.silverpeas.core.calendar.VisibilityLevel;
import org.silverpeas.core.calendar.ical4j.ICal4JDateCodec;
import org.silverpeas.core.calendar.ical4j.ICal4JImporter;
import org.silverpeas.core.calendar.ical4j.ICal4JRecurrenceCodec;
import org.silverpeas.core.date.Period;
import org.silverpeas.core.date.TimeUnit;
import org.silverpeas.core.importexport.ImportDescriptor;
import org.silverpeas.core.importexport.ImportException;
import org.silverpeas.core.persistence.datasource.OperationContext;
import org.silverpeas.core.persistence.datasource.model.jpa.JpaPersistOperation;
import org.silverpeas.core.persistence.datasource.model.jpa.JpaUpdateOperation;
import org.silverpeas.core.test.unit.extention.JEETestContext;
import org.silverpeas.kernel.test.extension.EnableSilverTestEnv;
import org.silverpeas.kernel.test.annotations.TestManagedBean;
import org.silverpeas.kernel.test.annotations.TestManagedBeans;
import org.silverpeas.kernel.test.annotations.TestedBean;
import org.silverpeas.core.util.Charsets;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.silverpeas.core.util.CollectionUtil.asList;
import static org.silverpeas.kernel.util.StringUtil.defaultStringIfNotDefined;

/**
 * @author Yohann Chastagnier
 */
@EnableSilverTestEnv(context = JEETestContext.class)
@TestManagedBeans({JpaPersistOperation.class, JpaUpdateOperation.class})
class ICal4JExchangeImportTest {

  @TestManagedBean
  private final ICal4JDateCodec dateCodec = new ICal4JDateCodec();
  @SuppressWarnings("unused")
  @TestManagedBean
  private final ICal4JRecurrenceCodec recurrenceCodec = new ICal4JRecurrenceCodec(dateCodec);
  @SuppressWarnings("unused")
  @TestedBean
  private ICal4JImporter iCalendarImporter;
  private User creator;

  private final BiConsumer<Pair<CalendarEvent, List<CalendarEventOccurrence>>, Pair<CalendarEvent,
      List<CalendarEventOccurrence>>>
      defaultAssert = (actual, expected) -> {
    CalendarEvent actualEvent = actual.getLeft();
    CalendarEvent expectedEvent = expected.getLeft();
    List<CalendarEventOccurrence> actualOccurrences = actual.getRight();
    List<CalendarEventOccurrence> expectedOccurrences = expected.getRight();
    verify(actualEvent, expectedEvent, actualOccurrences, expectedOccurrences);
  };

  @BeforeEach
  public void setup() {
    creator = mock(User.class);
    when(creator.getId()).thenReturn("creatorId");
    when(creator.getDisplayedName()).thenReturn("Creator Test");
    when(creator.getEmailAddress()).thenReturn("creator.test@silverpeas.org");
    when(UserProvider.get().getUser(creator.getId())).thenReturn(creator);

    OperationContext.fromUser("0");
  }

  @Test
  void undefinedListOfEvents() {
    assertThrows(IllegalArgumentException.class, () ->
        ImportDescriptor.withInputStream(null));
  }

  @Test
  void emptyFile() {
    assertThrows(ImportException.class,
        () -> importAndVerifyResult("ical4j_import_empty_file.txt", emptyList(), defaultAssert));
  }

  @Test
  void fileWithoutEvent() throws ImportException {
    importAndVerifyResult("ical4j_import_no_event.txt", emptyList(), defaultAssert);
  }

  @Test
  void verifyDateConversions() throws ImportException {
    CalendarEvent event1 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-14"), date("2016-12-16")))
        .withExternalId("EVENT-UUID-LOCAL-DATE")
        .withTitle("EVENT-TITLE")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    CalendarEvent event2 = CalendarEventStubBuilder
        .from(Period.between(
            datetime("2017-01-12T23:15:00Z"),
            datetime("2017-01-13T00:30:00Z")))
        .withExternalId("EVENT-UUID-UTC")
        .withTitle("EVENT-TITLE")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    CalendarEvent event3 = CalendarEventStubBuilder
        .from(Period.between(
            datetime("2017-01-12T16:15:00Z"),
            datetime("2017-01-12T23:30:00Z")))
        .withExternalId("EVENT-UUID-START-WITH-TIMEZONE-END-ON-DEFAULT-TIMEZONE")
        .withTitle("EVENT-TITLE")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    CalendarEvent event4 = CalendarEventStubBuilder
        .from(Period.between(
            datetime("2017-01-13T00:30:00Z"),
            datetime("2017-01-13T01:30:00Z")))
        .withExternalId("EVENT-UUID-UTC-START-EQUALS-END")
        .withTitle("EVENT-TITLE")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    CalendarEvent event5 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-16"), date("2016-12-17")))
        .withExternalId("EVENT-UUID-LOCAL-DATE-ONE-DAY-WITH-START-AND-END")
        .withTitle("EVENT-TITLE")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    CalendarEvent event6 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-16"), date("2016-12-17")))
        .withExternalId("EVENT-UUID-LOCAL-DATE-ONE-DAY-WITH-START-AND-END-RELAXED")
        .withTitle("EVENT-TITLE")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    CalendarEvent event7 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-16"), date("2016-12-17")))
        .withExternalId("EVENT-UUID-LOCAL-DATE-ONE-DAY-START-ONLY")
        .withTitle("EVENT-TITLE")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    importAndVerifyResult("ical4j_import_to_verify_date_conversion.txt",
        asList(event1, event2, event3, event4, event5, event6, event7), defaultAssert);
  }

  @Test
  void simpleOneOfTwoDaysDuration() throws ImportException {
    CalendarEvent event = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-14"), date("2016-12-16")))
        .withExternalId("EVENT-UUID").withTitle("EVENT-TITLE")
        .withDescription("EVENT-DESCRIPTION <a href=\"#\">Click me...</a> !!!")
        .withLocation("Grenoble")
        .withAttribute("url", "http://www.silverpeas.org/events/EVENT-UUID")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    importAndVerifyResult("ical4j_import_simple_two_days_duration.txt", singletonList(event),
        defaultAssert);
  }

  @Test
  void categorizedOneOfOneDayDuration() throws ImportException {
    CalendarEvent event = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-25"), date("2016-12-25")))
        .withExternalId("EVENT-UUID").withTitle("EVENT-TITLE-CATEGORIZED")
        .withCategories("Work", "Project")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).build();

    importAndVerifyResult("ical4j_import_categorized_one_day_duration.txt", singletonList(event),
        defaultAssert);
  }

  @Test
  void severalOfOneHourDurationAndDailyRecurrence() throws ImportException {
    CalendarEvent event1 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T12:32:00Z"),
            datetime("2016-12-15T13:32:00Z")))
        .withExternalId("EXT-EVENT-UUID-1")
        .withTitle("EVENT-TITLE-1")
        .withPriority(Priority.HIGH)
        .withVisibilityLevel(VisibilityLevel.PRIVATE)
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(TimeUnit.DAY)).build();
    CalendarEvent event2 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T13:50:00Z"),
            datetime("2016-12-15T14:50:00Z")))
        .withExternalId("EVENT-UUID-2")
        .withTitle("EVENT-TITLE-2")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(3, TimeUnit.DAY)).build();
    CalendarEvent event3 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T15:27:00Z"),
            datetime("2016-12-15T16:27:00Z")))
        .withExternalId("EVENT-UUID-3").withTitle("EVENT-TITLE-3")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(
            Recurrence.every(4, TimeUnit.DAY).until(datetime("2016-12-31T15:27:00Z")))
        .build();
    CalendarEvent event4 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T18:45:00Z"),
            datetime("2016-12-15T20:15:00Z")))
        .withExternalId("EVENT-UUID-4").withTitle("EVENT-TITLE-4")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(2, TimeUnit.DAY).until(10)).build();
    CalendarEvent event5 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T20:00:00Z"),
            datetime("2016-12-15T20:15:00Z")))
        .withExternalId("EVENT-UUID-5").withTitle("EVENT-TITLE-5")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(
            Recurrence.every(TimeUnit.DAY)
                .excludeEventOccurrencesStartingAt(
                    date("2016-12-18"),
                    date("2016-12-20"),
                    date("2016-12-25"))).build();
    CalendarEvent event6 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T20:30:00Z"),
            datetime("2016-12-15T20:45:00Z")))
        .withExternalId("EVENT-UUID-6").withTitle("EVENT-TITLE-6")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(
            Recurrence.every(2, TimeUnit.WEEK)
                .on(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)).build();
    CalendarEvent event7 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T21:00:00Z"),
            datetime("2016-12-15T21:15:00Z")))
        .withExternalId("EVENT-UUID-7").withTitle("EVENT-TITLE-7")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(
            Recurrence.every(1, TimeUnit.MONTH).on(DayOfWeekOccurrence.nth(3, DayOfWeek.FRIDAY)))
        .build();
    CalendarEvent event8 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T21:15:00Z"),
            datetime("2016-12-15T21:30:00Z")))
        .withExternalId("EVENT-UUID-8").withTitle("EVENT-TITLE-8")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(TimeUnit.MONTH)).build();
    CalendarEvent event9 = CalendarEventStubBuilder.from(Period.between(
            datetime("2016-12-15T21:45:00Z"),
            datetime("2016-12-15T22:00:00Z")))
        .withExternalId("EVENT-UUID-9").withTitle("EVENT-TITLE-9")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(TimeUnit.YEAR)).build();

    importAndVerifyResult("ical4j_import_several_with_recurrence.txt",
        asList(event1, event2, event3, event4, event5, event6, event7, event8, event9),
        defaultAssert);
  }

  @Test
  void severalOnAllDaysAndDailyRecurrence() throws ImportException {
    CalendarEvent event1 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EXT-EVENT-UUID-1")
        .withTitle("EVENT-TITLE-1").withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(TimeUnit.DAY)).build();
    CalendarEvent event2 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-2").withTitle("EVENT-TITLE-2")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(3, TimeUnit.DAY)).build();
    CalendarEvent event3 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-3").withTitle("EVENT-TITLE-3")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(4, TimeUnit.DAY).until(date("2016-12-31")))
        .build();
    CalendarEvent event4 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-4").withTitle("EVENT-TITLE-4")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(2, TimeUnit.DAY).until(10)).build();
    CalendarEvent event5 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-5").withTitle("EVENT-TITLE-5")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).withRecurrence(
            Recurrence.every(TimeUnit.DAY)
                .excludeEventOccurrencesStartingAt(date("2016-12-18"),
                    date("2016-12-20"), date("2016-12-25"))).build();
    CalendarEvent event6 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-6").withTitle("EVENT-TITLE-6")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).withRecurrence(
            Recurrence.every(2, TimeUnit.WEEK)
                .on(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)).build();
    CalendarEvent event7 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-7").withTitle("EVENT-TITLE-7")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z")).withRecurrence(
            Recurrence.every(1, TimeUnit.MONTH).on(DayOfWeekOccurrence.nth(3, DayOfWeek.FRIDAY)))
        .build();
    CalendarEvent event8 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-8").withTitle("EVENT-TITLE-8")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(TimeUnit.MONTH)).build();
    CalendarEvent event9 = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-15"), date("2016-12-15")))
        .withExternalId("EVENT-UUID-9").withTitle("EVENT-TITLE-9")
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        .withRecurrence(Recurrence.every(TimeUnit.YEAR)).build();

    importAndVerifyResult("ical4j_import_several_with_recurrence_on_all_day.txt",
        asList(event1, event2, event3, event4, event5, event6, event7, event8, event9),
        defaultAssert);
  }

  @Test
  void simpleOneOfTwoDayDurationWithAttendees() throws ImportException {
    User user = mock(User.class);
    when(user.getId()).thenReturn("userId");
    when(user.getDisplayedName()).thenReturn("User Test");
    when(user.getEmailAddress()).thenReturn("user.test@silverpeas.org");
    CalendarEvent event = CalendarEventStubBuilder
        .from(Period.between(date("2016-12-14"), date("2016-12-16")))
        .withExternalId("EVENT-UUID-ATTENDEES")
        .withTitle("EVENT-TITLE")
        .withCreator(creator)
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        /*

        NOT YET HANDLED

        .withAttendee(user, a -> {
            a.setPresenceStatus(OPTIONAL);
            a.accept();
        })
        .withAttendee("external.1@silverpeas.org", a ->
            a.setPresenceStatus(INFORMATIVE))
        .withAttendee("external.2@silverpeas.org", a ->
            a.decline())

        */
        .build();

    importAndVerifyResult("ical4j_import_simple_two_days_duration_with_attendees.txt",
        singletonList(event), defaultAssert);
  }

  @Test
  void oneWithRecurrenceAndWithAttendees() throws ImportException {
    User user = mock(User.class);
    when(user.getId()).thenReturn("userId");
    when(user.getDisplayedName()).thenReturn("User Test");
    when(user.getEmailAddress()).thenReturn("user.test@silverpeas.org");
    CalendarEvent event = CalendarEventStubBuilder
        .from(Period.between(date("2016-11-30"), date("2016-11-30")))
        .withExternalId("EVENT-UUID-RECURRENCE-ATTENDEES")
        .withTitle("EVENT-TITLE")
        .withRecurrence(Recurrence.every(TimeUnit.DAY).until(10))
        .withCreator(creator)
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        /*

        NOT YET HANDLED

        .withAttendee(user, a -> {
            a.setPresenceStatus(OPTIONAL);
            a.accept();
        })
        .withAttendee("external.1@silverpeas.org", a ->
            a.setPresenceStatus(INFORMATIVE))
        .withAttendee("external.2@silverpeas.org", a ->
            a.decline())

        */
        .build();

    importAndVerifyResult("ical4j_import_one_with_recurrence_and_with_attendees.txt",
        singletonList(event), defaultAssert);
  }

  @Test
  void oneWithRecurrenceAndWithAttendeesWhichAnsweredOnDate() throws ImportException {
    User user = mock(User.class);
    when(user.getId()).thenReturn("userId");
    when(user.getDisplayedName()).thenReturn("User Test");
    when(user.getEmailAddress()).thenReturn("user.test@silverpeas.org");
    when(UserProvider.get().getUser(user.getId())).thenReturn(user);
    final String externalAttendeeId_1 = "external.1@silverpeas.org";
    final String externalAttendeeId_2 = "external.2@silverpeas.org";
    CalendarEvent event = CalendarEventStubBuilder
        .from(Period.between(date("2016-11-30"), date("2016-11-30")))
        .withExternalId("EVENT-UUID-RECURRENCE-ATTENDEES-ON-DATE-ANSWER")
        .withTitle("EVENT-TITLE")
        .withRecurrence(Recurrence.every(TimeUnit.DAY).until(10))
        .withCreator(creator)
        .withCreationDate(datetime("2016-12-01T14:30:00Z"))
        .withLastUpdateDate(datetime("2016-12-02T09:00:00Z"))
        /*

        NOT YET HANDLED

        .withAttendee(user, a -> {
            a.setPresenceStatus(OPTIONAL);
            a.accept();
        })
        .withAttendee(externalAttendeeId_1, a ->
            a.setPresenceStatus(INFORMATIVE))
        .withAttendee(externalAttendeeId_2, a ->
            a.decline())

        */
        .withOccurrenceOn(Period.between(date("2016-12-06"), date("2016-12-06")),
            o -> {
              o.getAttendees().get(user.getId()).ifPresent(Attendee::decline);
              o.getAttendees().get(externalAttendeeId_1).ifPresent(Attendee::decline);
            })
        .withOccurrenceOn(Period.between(date("2016-12-09"), date("2016-12-10")),
            o -> o.getAttendees().get(user.getId()).ifPresent(Attendee::tentativelyAccept))
        .withOccurrenceOn(Period.between(date("2016-12-10"), date("2016-12-11")),
            o -> o.getAttendees().get(externalAttendeeId_2).ifPresent(Attendee::accept))
        .build();

    importAndVerifyResult(
        "ical4j_import_one_with_recurrence_and_with_attendees_which_answered_on_date.txt",
        singletonList(event), defaultAssert);
  }

  /**
   * Centralization of verification.<br>
   * <p>
   * The mechanism is the following:<br>
   * <p/>
   * the first parameter represent the name of the file that contains events to import and the second one is the list of
   * expected calendar events.<br> Each lines starting with '#' character is ignored.
   * </p>
   *
   * @param fileNameOfImport the name of the file that contains events to import.
   */
  @SuppressWarnings("Duplicates")
  private void importAndVerifyResult(String fileNameOfImport, List<CalendarEvent> expectedEvents,
                                     BiConsumer<Pair<CalendarEvent, List<CalendarEventOccurrence>>, Pair<CalendarEvent,
                                         List<CalendarEventOccurrence>>> assertConsumer) throws ImportException {

    Map<String, Pair<CalendarEvent, List<CalendarEventOccurrence>>> result = new HashedMap<>();

    iCalendarImporter.imports(ImportDescriptor.withInputStream(new ByteArrayInputStream(
            getFileContent(fileNameOfImport).getBytes(StandardCharsets.UTF_8))),
        events -> result.putAll(events.collect(
            Collectors.toMap(p -> p.getLeft().getExternalId(), Function.identity()))));

    Map<String, Pair<CalendarEvent, List<CalendarEventOccurrence>>> expected =
        expectedEvents.stream().collect(Collectors
            .toMap(CalendarEvent::getExternalId, e -> Pair.of(e, e.getPersistedOccurrences())));

    assertThat("The expected list contains several event with same external id", expected.size(),
        is(expectedEvents.size()));


    assertThat(result.keySet(), containsInAnyOrder(expected.keySet().toArray()));

    result.forEach((i, actualResult) -> {
      Pair<CalendarEvent, List<CalendarEventOccurrence>> expectedResult = expected.get(i);
      assertConsumer.accept(actualResult, expectedResult);
    });
  }

  private void verify(final CalendarEvent actualEvent, final CalendarEvent expectedEvent,
                      final List<CalendarEventOccurrence> actualOccurrences,
                      final List<CalendarEventOccurrence> expectedOccurrences) {
    verify(actualEvent, expectedEvent);
    assertThat(actualOccurrences, hasSize(expectedOccurrences.size()));
    expectedOccurrences.forEach(expected -> {
      String reason =
          expected.getCalendarEvent().getExternalId() + " - occ - " + expected.getId() + " - ";
      CalendarEventOccurrence actual = actualOccurrences.stream()
          .filter(o -> expected.getOriginalStartDate().equals(o.getOriginalStartDate())).findFirst()
          .orElse(null);
      assertThat(reason + "getId", actual.getId(), is(expected.getId()));
      assertThat(reason + "getId", actual.getOriginalStartDate(), is(expected.getOriginalStartDate()));
      assertThat(reason + "getCalendar", actual.getCalendarEvent().getCalendar(), is(expected.getCalendarEvent().getCalendar()));
      assertThat(reason + "getStartDate", actual.getStartDate(), is(expected.getStartDate()));
      assertThat(reason + "getEndDate", actual.getEndDate(), is(expected.getEndDate()));
      assertThat(reason + "getTitle", actual.getTitle(), is(expected.getTitle()));
      assertThat(reason + "getDescription", actual.getDescription(), is(defaultStringIfNotDefined(expected.getDescription())));
      assertThat(reason + "getLocation", actual.getLocation(), is(defaultStringIfNotDefined(expected.getLocation())));
      assertThat(reason + "getAttendees", actual.getAttendees().stream().collect(Collectors.toSet()), is(expected.getAttendees().stream().collect(Collectors.toSet())));
      assertThat(reason + "getCategories", actual.getCategories(), is(expected.getCategories()));
      assertThat(reason + "getAttributes", actual.getAttributes(), is(expected.getAttributes()));
      assertThat(reason + "getPriority", actual.getPriority(), is(expected.getPriority()));
      assertThat(reason + "getVisibilityLevel", actual.getVisibilityLevel(), is(expected.getVisibilityLevel()));
      verifyComponent(reason, actual.asCalendarComponent(), expected.asCalendarComponent());
    });
  }

  private void verify(final CalendarEvent actual, final CalendarEvent expected) {
    String reason = actual.getExternalId() + " - ";
    assertThat(reason + "getId", actual.getId(), nullValue());
    assertThat(reason + "getId", actual.getId(), is(expected.getId()));
    assertThat(reason + "getExternalId", actual.getExternalId(), is(expected.getExternalId()));
    assertThat(reason + "getCalendar", actual.getCalendar(), is(expected.getCalendar()));
    assertThat(reason + "getStartDate", actual.getStartDate(), is(expected.getStartDate()));
    assertThat(reason + "getEndDate", actual.getEndDate(), is(expected.getEndDate()));
    assertThat(reason + "getTitle", actual.getTitle(), is(expected.getTitle()));
    assertThat(reason + "getDescription", actual.getDescription(), is(defaultStringIfNotDefined(expected.getDescription())));
    assertThat(reason + "getLocation", actual.getLocation(), is(defaultStringIfNotDefined(expected.getLocation())));
    assertThat(reason + "isRecurrent", actual.isRecurrent(), is(expected.isRecurrent()));
    assertThat(reason + "getRecurrence", actual.getRecurrence(), is(expected.getRecurrence()));
    assertThat(reason + "getAttendees", actual.getAttendees().stream().collect(Collectors.toSet()), is(expected.getAttendees().stream().collect(Collectors.toSet())));
    assertThat(reason + "getCategories", actual.getCategories(), is(expected.getCategories()));
    assertThat(reason + "getAttributes", actual.getAttributes(), is(expected.getAttributes()));
    assertThat(reason + "getPriority", actual.getPriority(), is(expected.getPriority()));
    assertThat(reason + "getVisibilityLevel", actual.getVisibilityLevel(), is(expected.getVisibilityLevel()));
    assertThat(reason + "getCreationDate", actual.getCreationDate(), is(expected.getCreationDate()));
    assertThat(reason + "getLastUpdateDate", actual.getLastUpdateDate(), is(expected.getLastUpdateDate()));
    verifyComponent(reason, actual.asCalendarComponent(), expected.asCalendarComponent());
  }

  private void verifyComponent(final String parentReason, final CalendarComponent actual,
                               final CalendarComponent expected) {
    String reason = parentReason + "cmp - ";
    assertThat(reason + "getId", actual.getId(), nullValue());
    assertThat(reason + "getId", actual.getId(), is(expected.getId()));
    assertThat(reason + "getCalendar", actual.getCalendar(), is(expected.getCalendar()));
    assertThat(reason + "getStartDate", actual.getPeriod().getStartDate(), is(expected.getPeriod().getStartDate()));
    assertThat(reason + "getEndDate", actual.getPeriod().getEndDate(), is(expected.getPeriod().getEndDate()));
    assertThat(reason + "getTitle", actual.getTitle(), is(expected.getTitle()));
    assertThat(reason + "getDescription", actual.getDescription(), is(defaultStringIfNotDefined(expected.getDescription())));
    assertThat(reason + "getLocation", actual.getLocation(), is(defaultStringIfNotDefined(expected.getLocation())));
    assertThat(reason + "getAttendees", actual.getAttendees().stream().collect(Collectors.toSet()), is(expected.getAttendees().stream().collect(Collectors.toSet())));
    assertThat(reason + "getAttributes", actual.getAttributes(), is(expected.getAttributes()));
    assertThat(reason + "getPriority", actual.getPriority(), is(expected.getPriority()));
    assertThat(reason + "getCreationDate", actual.getCreationDate(), is(expected.getCreationDate()));
    assertThat(reason + "getLastUpdateDate", actual.getLastUpdateDate(), is(expected.getLastUpdateDate()));
  }

  private String getFileContent(String fileName) {
    try (InputStream fileStream = getClass().getResourceAsStream(fileName)) {
      return String.join("\n", IOUtils.readLines(Objects.requireNonNull(fileStream), Charsets.UTF_8));
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  private static LocalDate date(String date) {
    return LocalDate.parse(date);
  }

  private static OffsetDateTime datetime(String date) {
    return OffsetDateTime.parse(date);
  }

}